#include <sched.h>
#include <stdio.h>
#include "cpu.h"
#include "keyboard.h"
#include "process.h"
#include "pcb.h"

#define SYSTEM_CALL 12
#define PROCESS_1 1 //No Significance to these numbers, just using to avoid "magic numbers."
#define PROCESS_2 2
#define PROCESS_3 3

// Semaphore structure to use for producer/consumer processes, contains a blocking queue and the semaphore owner.
typedef struct semaphore_str {
    PCB owner;
    PQueue *queue;
} SemaphoreSTR;

// A pointer to the semaphore structure.
typedef SemaphoreSTR *SemPtr;

// Global CPU pointer.
CPUPtr my_cpu;

// The PCB of the current running process.
PCB current_process;

// Global semaphore used in producer and consumer processes.
SemPtr sem;

// The idle process.
PCB idl;

// The shared memory allocated for the producer(s) and consumer(s).
int product = 0;

// Only The main thread has access to these blocked queues so no mutexes are required.
// Note should this not be the case, mutexes need to be created to synchonrize the data.
PQueue *keyboard_blocked_queue;
PQueue *io_device1_block_queue;
PQueue *io_device2_block_queue;

// This function returns a pointer to memory allocated for the CPU type.
CPUPtr cpu_init(pthread_mutex_t *the_run_lock, pthread_cond_t *the_run_cond, pthread_mutex_t *the_step_lock,
                PQueue *the_ready_queue, PQueue *the_int_controller, pthread_mutex_t *the_interrupt_lock)
{
    CPUPtr temp = (CPUPtr) malloc(sizeof(CPUStr));
    if (temp == NULL)
    {
        perror("malloc failed on cpu_init");
        exit(EXIT_FAILURE);
    }
    temp->run_lock = the_run_lock;
    temp->run_cond = the_run_cond;
    temp->step_lock = the_step_lock;
    temp->pc_reg = 0;
    temp->run_var = INTERRUPT_SERVICE;
    temp->ready_queue = the_ready_queue;
    temp->int_controller = the_int_controller;
    temp->interrupt_lock = the_interrupt_lock;

    return temp;
}

// A 'clean-up' function to de-allocate memory.
void cpu_destroy(CPUPtr the_cpu)
{
    if (the_cpu != NULL)
    {
        pthread_mutex_destroy(the_cpu->run_lock);
        pthread_cond_destroy(the_cpu->run_cond);
        pthread_mutex_destroy(the_cpu->step_lock);
        pthread_mutex_destroy(the_cpu->interrupt_lock);
        queue_free(the_cpu->ready_queue);
        queue_free(the_cpu->int_controller);

        free(the_cpu);
        the_cpu = NULL;
    }
}

// This is the scheduler of the operating system.
void sched_func(int the_interrupt)
{
    printf("Scheduler has been called because a ");

    // A switch statement to handle the various reasons the scheduler was called.
    switch (the_interrupt)
    {
        PCB next_process;

        case TIMER_INTERRUPT:
            printf("Timer interrupt.\n");
            contex_switch();
            break;

        case KEYBOARD_INTERRUPT:
            printf("Keyboard interrupt.\n");
            ready_queue_pri_check();
            break;

        case SEM_BLOCK:
            printf("Semaphore block.\n");
            current_process->next_step = my_cpu->pc_reg; //Saves the current pc in the pcb.
            contex_switch();
            break;

        case IO_INTERRUPT_1:
            printf("I/O interrupt from device 1.\n");
            ready_queue_pri_check();
            break;

        case IO_INTERRUPT_2:
            printf("I/O interrupt from device 2.\n");
            ready_queue_pri_check();
            break;

        case IO_REQUEST:
            printf("I/O request call.\n");
            contex_switch();
        default:
            break;
    }

    //interrupt = 20; //no interrupt
    pthread_cond_broadcast(my_cpu->run_cond); //Tells the process with the id of run_var to run.
}

//Helper function to reduce code redundancy
void contex_switch()
{
    PCB next_process = (PCB)queue_dequeue(my_cpu->ready_queue); //Grabs the next pcb from the ready_queue
    my_cpu->pc_reg = next_process->next_step; //Sets the pc reg to the address of the next process
    next_process->state = STATE_RUNNING; //Set the state to running
    printf("Process %d was put into RUNNING state.\n", next_process->pid);
    my_cpu->run_var = next_process->pid; //part of context change (allows the process to run).
    printf("Contex switch from Process %d to Process %d.\n", current_process->pid, next_process->pid);
    current_process = next_process; //Context change
}

void ready_queue_pri_check()
{
    PCB next_process = (PCB)queue_front(my_cpu->ready_queue); //Grabs the next pcb without removing it from ready_queue.

    //If the next process has higher priority then do a contex switch.
    if (next_process->proc->priority < current_process->proc->priority)
    {
        current_process->next_step = my_cpu->pc_reg; //Saves the current pc in the pcb.
        current_process->state = STATE_READY; //Sets the state to ready
        queue_enqueue(my_cpu->ready_queue, current_process, 1); //then puts the current process into the ready queue
        contex_switch();
    }
    else // no context switch
    {
        current_process->state = STATE_RUNNING;
        printf("Process %d put back into RUNNING state.\n", current_process->pid);
    }
}

// This function is called by the main CPU when a timer interrupt occurs, it finishes by calling the scheduler.
void timer_int_service_routine()
{
    printf("Timer interrupt recieved by CPU.\n");
    current_process->next_step = my_cpu->pc_reg; //Saves the current pc in the pcb.
    current_process->state = STATE_READY; //Sets the state to ready
    queue_enqueue(my_cpu->ready_queue, current_process, 1); //then puts the current process into the ready queue
    printf("Timer trap has put Process %d in READY state an placed into ready queue.\n", current_process->pid);
    sched_func(TIMER_INTERRUPT);
}

// This function is called by the main CPU when a i/o interrupt occurs, it finishes by calling the scheduler.
void io_int_service_routine(int the_io_interrupt)
{
    printf("I/O interrupt service trapped called.\n");
    current_process->state = STATE_INTERRUPTED; //Places current process into interrupt state.
    printf("Process %d has been placed into INTERRUPTED state.\n", current_process->pid);
    switch (the_io_interrupt)
    {
        case KEYBOARD_INTERRUPT:
            io_int_check_blocked_queue(keyboard_blocked_queue);
            sched_func(KEYBOARD_INTERRUPT);
            break;
        case IO_INTERRUPT_1:
            io_int_check_blocked_queue(io_device1_block_queue);
            sched_func(IO_INTERRUPT_1);
            break;
        case IO_INTERRUPT_2:
            io_int_check_blocked_queue(io_device2_block_queue);
            sched_func(IO_INTERRUPT_2);
            break;
        default:
            printf("Not a valid I/O interrupt signal.\n");
            break;
    }
}

//helper function to reduce code redundancy
void io_int_check_blocked_queue(PQueue *the_blocked_queue)
{
    if (queue_has_next(the_blocked_queue))
    {
        PCB blocked = (PCB)queue_dequeue(the_blocked_queue); //retrieve the process
        blocked->state = STATE_READY;
        queue_enqueue(my_cpu->ready_queue, blocked, QUEUE_NORM_PRI); //put the process back into the ready queue
        printf("Process %d has been taken out of the blocked queue and placed back into ready queue.\n", blocked->pid);
    }
    else
    {
        printf("No processes were waiting on this interrupt.\n");
    }
}

void exit_func()
{
    my_cpu->run_var = EXIT_VAR;
    printf("\nThe exit key has been detected.\nThank you for using this simulator.\n");
    printf("Goodbye.\n");
    exit(0);
}

// Creates and returns a producer process.
Process make_prod_con()
{
    int no_steps = (rand() % 30) + 5;
    int request = (rand() % (no_steps - 4)) + 1;

    // Creates an array to hold each request to add to the returned process.
    int *temp_array = (int *) malloc ((sizeof(int))*3);
    temp_array[0] = request++;
    temp_array[1] = request++;
    temp_array[2] = request++;
    Process temp = process_init(PRODUCER, no_steps, 3, temp_array, QUEUE_NORM_PRI);

    return temp;
}

//Creates and returns a basic process
Process make_process()
{
    int no_steps = (rand() % 30) + 5;
    int request = (rand() % no_steps-1) + 1;
    int *temp_array = (int *) malloc (sizeof(int));
    temp_array[0] = request;
    Process temp = process_init(IDL, no_steps, 1, temp_array, QUEUE_NORM_PRI);

    return temp;
}

// Attempts to get ownership of the producer/consumer semaphore.
void get_sem()
{
    // If no-one owns the semaphore, the current process will receive ownership.
    if (sem->owner == idl)
    {
        sem->owner = current_process;
        printf("Process %d was given ownership of the semaphore.\n", current_process->pid);
    }

    // If this statement is not entered, the current process already owns the semaphore.
    // If it is entered, the current process will enter the semaphore's blocking queue and become blocked.
    else if(sem->owner != current_process)
    {
        current_process->next_step = my_cpu->pc_reg;
        queue_enqueue(sem->queue, current_process, QUEUE_NORM_PRI);
        current_process->state = STATE_BLOCKED;
        current_process->waiting_on = sem->queue;
        printf("Process %d did not have ownership of the semaphore.\n", current_process->pid);
        sched_func(SEM_BLOCK);
    }
}

// Releases ownership of the producer/consumer semaphore.
void release_sem()
{
    // If this was not true, the process would not own the semaphore, so it cannot release it.
    if (sem->owner == current_process)
    {
        // If other processes are waiting for the semaphore, hand over ownership to the next process.
        if (queue_size(sem->queue))
        {
            PCB next_process = (PCB) queue_dequeue(sem->queue);
            sem->owner = next_process;
            queue_enqueue(my_cpu->ready_queue, next_process, QUEUE_NORM_PRI);
            printf("Process %d has given up the semaphore to Process %d.\n", current_process->pid, next_process->pid);
        }
        // Otherwise simply release the semaphore.
        else
        {
            sem->owner = idl;
            printf("Process %d has given up the semaphore.\n", current_process->pid);
        }
    }
}

// Called after the producer has access to the critical memory section. Produces a random number and puts it in shared memory.
void prod_func()
{
    product = rand();
    printf("Producer process %d has written %d to shared memory.\n", current_process->pid, product);
}

// Called after the consumer has access to the critical memory section. Consumes the random number.
void consum_func()
{
    printf("Consumer process %d has read %d from shared memory.\n", current_process->pid, product);
}

// Called when the calculate function is called.
void calc_func()
{
    int calc;
    calc = rand();
    calc = rand() % calc;
    printf("Process %d has calculated %d. \n", current_process->pid, calc);
}

void io_sys_request(int the_type)
{
    current_process->next_step = my_cpu->pc_reg;
    current_process->state = STATE_BLOCKED;
    printf("Process %d has requested I/O.\n", current_process->pid);
    pthread_t io_key;
    switch (the_type)
    {
        case KEYBOARD_INTERRUPT:
            queue_enqueue(keyboard_blocked_queue, current_process, QUEUE_NORM_PRI);
            current_process->waiting_on = keyboard_blocked_queue;
            printf("Process %d's state has been set to BLOCKED, and has been placed in they keyboard blocked queue.\n", current_process->pid);
            sched_func(IO_REQUEST);
            break;

        case IO_INTERRUPT_1:
            queue_enqueue(keyboard_blocked_queue, current_process, QUEUE_NORM_PRI);
            current_process->waiting_on = keyboard_blocked_queue;
            printf("Process %d's state has been set to BLOCKED, and has been placed in they I/O device #1 blocked queue.\n", current_process->pid);
            sched_func(IO_REQUEST);
            pthread_create(&io_key, NULL, io_1_trap_service_routine, my_cpu);
            break;

        case IO_INTERRUPT_2:
            queue_enqueue(keyboard_blocked_queue, current_process, QUEUE_NORM_PRI);
            current_process->waiting_on = keyboard_blocked_queue;
            printf("Process %d's state has been set to BLOCKED, and has been placed in they I/O device #2 blocked queue.\n", current_process->pid);
            sched_func(IO_REQUEST);
            pthread_create(&io_key, NULL, io_2_trap_service_routine, my_cpu);
            break;
    }
}

// Called when the I/O process is running.
void *io_1_trap_service_routine(void *the_cpu)
{
    CPUPtr temp_cpu = (CPUPtr)the_cpu;
    int interrupt_signal = IO_INTERRUPT_1;
    printf("I/O device #1 has been activated.\n");
    int random = (rand() %1000) + 1000; //milisecs
    msleep(random);
    pthread_mutex_lock(temp_cpu->interrupt_lock);
    queue_enqueue(temp_cpu->int_controller, &interrupt_signal, QUEUE_NORM_PRI);
    pthread_mutex_unlock(temp_cpu->interrupt_lock);
    printf("I/O device #1 has completed and generated an interrupt.\n");

    return NULL;
}

void *io_2_trap_service_routine(void *the_cpu)
{
    CPUPtr temp_cpu = (CPUPtr)the_cpu;
    int interrupt_signal = IO_INTERRUPT_1;
    printf("I/O device #2 has been activated.\n");
    int random = (rand() %1000) + 500; //milisecs
    msleep(random);
    pthread_mutex_lock(temp_cpu->interrupt_lock);
    queue_enqueue(temp_cpu->int_controller, &interrupt_signal, QUEUE_NORM_PRI);
    pthread_mutex_unlock(temp_cpu->interrupt_lock);
    printf("I/O device #2 has completed and generated an interrupt.\n");

    return NULL;
}

// Code retrieved from http://cc.byexamples.com/2007/05/25/nanosleep-is-better-than-sleep-and-usleep/
// Part of a milisecond sleep timer.
int __nsleep(const struct timespec *req, struct timespec *rem)
{
    struct timespec temp_rem;
    if(nanosleep(req,rem)==-1)
        __nsleep(rem,&temp_rem);
    else
        return 1;
}

// Code retrieved from http://cc.byexamples.com/2007/05/25/nanosleep-is-better-than-sleep-and-usleep/
// Part of a milisecond sleep timer.
int msleep(unsigned long milisec)
{
    struct timespec req={0},rem={0};
    time_t sec=(int)(milisec/1000);
    milisec=milisec-(sec*1000);
    req.tv_sec=sec;
    req.tv_nsec=milisec*1000000L;
    __nsleep(&req,&rem);
    return 1;
}

// The main function in the OS.
int main(int argc, char *argv[])
{
    // The number of cycles the CPU has run through so far.
    int cycle_count = 0;
	int num_processes = 0;

    // A string containing the passed command line arguments.
	char * processes;

    // Verify arguments are passed.
	if (argc > 1) {
		processes = argv[1];
	}

	//Initialize all Queue pointers.
	keyboard_blocked_queue = queue_new();
	io_device1_block_queue = queue_new();
	io_device2_block_queue = queue_new();
	PQueue *ready_queue = queue_new();
    PQueue *int_controller = queue_new();
    PQueue *semaphore_queue = queue_new();

    // Declare all mutexes needed by the CPU internally.
    pthread_mutex_t run_lock;
    pthread_cond_t run_cond;
    pthread_mutex_t step_lock;
    pthread_mutex_t interrupt_lock;

    //pthread_cond_init(&interrupt_cond, NULL);

    // Initialize all mutexes needed by the CPU internally.
    pthread_mutex_init(&run_lock, NULL);
    pthread_cond_init(&run_cond, NULL);
    pthread_mutex_init(&step_lock, NULL);
    pthread_mutex_init(&interrupt_lock, NULL);

    sem = (SemPtr) malloc (sizeof(SemaphoreSTR));
    sem->queue = semaphore_queue;

    my_cpu = cpu_init(&run_lock, &run_cond, &step_lock, ready_queue, int_controller, &interrupt_lock);

	int * temp_array = (int *) malloc(8);

	int i;
	Process idl_process = process_init(IDL, 10, 0, temp_array, QUEUE_LOW_PRI);
	idl = pcb_init(0, 0, idl_process, my_cpu);
	queue_enqueue(ready_queue, idl, QUEUE_LOW_PRI);
	sem->owner = idl;


	// This parses the supplied command line arguments and creates the corresponding processes.
	for(i = 0; i < strlen(processes); i++) {
		Process new_process;
		PCB new_pcb;
		pthread_t key;
		if (processes[i] == 'p')
		{
			new_process = make_prod_con();
		} else if (processes[i] == 'c')
		{
			new_process = make_prod_con();
			new_process->type = CONSUMER;
		} else if (processes[i] == 'k')
		{
			new_process = make_process();
			new_process->type = KEYBOARD;
		} else if (processes[i] == 'l')
		{
		    new_process = make_process();
		    new_process->type = CALCULATE;
        } else if (processes[i] == 'i')
		{
			new_process = make_process();
			new_process->type = IO;
		}
		new_pcb = pcb_init(i+1, 0, new_process, my_cpu);
		queue_enqueue(ready_queue, new_pcb, QUEUE_NORM_PRI);
		pthread_create(&key, NULL, pcb_run, new_pcb);
	}

    current_process = (PCB)queue_dequeue(ready_queue);
    pthread_t timer_key;
    pthread_t keyboard_device_key;

    pthread_create(&timer_key, NULL, timer_start, my_cpu);
    pthread_create(&keyboard_device_key, NULL, keyboard_listen, my_cpu);
    while (my_cpu->run_var != EXIT_VAR)
    {
        // Sleep so the user may follow the onscreen data easier.
        msleep(500);
        cycle_count++;
        printf("Current cpu cycle %d.\n", cycle_count);
        pthread_mutex_lock(my_cpu->run_lock); // Critical section due to each process having its own thread, only need this scheduler running once at a time.
        pthread_mutex_lock(&interrupt_lock);

        // Check interrupts
        while (queue_has_next(int_controller))
        {
            int interrupt = *(int *)queue_dequeue(int_controller);
            switch (interrupt)
            {
                case TIMER_INTERRUPT:
                    timer_int_service_routine();
                    break;
                default:
                    io_int_service_routine(interrupt);
                    break;
            }
        }
        pthread_mutex_unlock(&interrupt_lock);
        pthread_mutex_lock(&step_lock);
        my_cpu->pc_reg++;
        int j;
        int flag = 0;

        // Process the various requests a process may have.
        for (j = 1; j < (sizeof(current_process->proc->requests)); j++)
        {
            int request_address = current_process->proc->requests[j-1];
            //printf ("\n\ncurrent process request addresses are %d", (sizeof(current_process->proc->requests)));
            if (my_cpu->pc_reg == request_address)
            {
                switch (current_process->proc->type)
                {
                    case PRODUCER:
                        switch (j)
                        {
                            case 1:
                                get_sem();
                                break;
                            case 2:
                                prod_func();
                                break;
                            case 3:
                                release_sem();
                                break;
                            default:
                                break;
                        }
                        break;

                    case CONSUMER:
                        switch (j)
                        {
                            case 1:
                            get_sem();
                            break;
                            case 2:
                            consum_func();
                            break;
                            case 3:
                            release_sem();
                            break;
                            default:
                            break;
                        }
                        break;

                    case KEYBOARD:
                        io_sys_request(KEYBOARD_INTERRUPT);
                        break;

                    case CALCULATE:
                        calc_func();
                        break;

                    default:
                        if (flag)
                        {
                            io_sys_request(IO_INTERRUPT_1);
                        }
                        else
                        {
                            io_sys_request(IO_INTERRUPT_2);
                        }
                        break;
                }
            }
        }
        pthread_mutex_unlock(&step_lock);
        pthread_mutex_unlock(my_cpu->run_lock);
    }
    return 0;
}
